<?php
/**
 * @name /lib/private/User.php
 * @author Michael Foss <michael@mikefoss.com>
 * @version 2011.03.26
 * 
 * A user class to manage users.
 */



/**
 * User class.
 *
 */
class User {
  /**
   * User id; unique identifier.
   *
   * @var string
   */
  var $UserId;
  /**
   * UserName; must be unique.
   *
   * @var string
   */
  var $UserName;
  /**
   * Password hash, encrypted using SHA1 encryption;
   * exactly 40 hexadecimal digits in length.
   *
   * @private
   * @var string
   */
  var $_PasswordHash;
  /**
    * The first name of the user.
    *
    * @var string
    */
  var $FirstName;
  /**
    * The last name of the user.
    *
    * @var string
    */
  var $LastName;
  /**
    * The phone number of the user.
    *
    * @var string
    */
  var $PhoneNumber;
  /**
    * The first street address of the user.
    *
    * @var string
    */
  var $Street1;
  /**
    * The second street address of the user;
    * optionally blank, unless Street3 is not.
    *
    * @var string
    */
  var $Street2;
  /**
    * The third street address of the user;
    * optionally blank.
    *
    * @var string
    */
  var $Street3;
  /**
    * The city of the user.
    *
    * @var string
    */
  var $City;
  /**
    * The state or province of the user.
    *
    * @var string
    */
  var $State;
  /**
    * The country of the user.
    *
    * @var string
    */
  var $Country;
  /**
    * The postal code of the user.
    *
    * @var string
    */
  var $PostalCode;
  /**
    * The roles of the user.
    *
    * @private
    * @var string[]
    */
  var $_Roles;
  /**
    * Whether or not the user is subscribed to the newsletter.
    *
    * @var bool
    */
  var $NewsletterSubscription;
  /**
    * Any administrative notes about the user.
    *
    * @var string
    */
  var $Notes;

  /* Methods:
    "GET":
    User[] GetUsers(int offset, int maxResults)
    bool Exists(int userId)
    string ToString()

    "SCUD":
    bool Load(int userId)
    bool Create(userName, email, password)
    bool Save()
    static bool Delete(int userId)

    Custom:
    bool HasDonationHistory()
    int GetUserIdByUserName(string userName)
    int GetUserIdByEmail(string email)
    bool SetPassword(string password)
    bool ValidatePassword(string password)
    bool HasRole(string role)
    bool GrantRole(string role)
    bool RevokeRole(string role)
    void RevokeAllRoles()
    static string[] GetRoles()
    static bool CreateRole(string name, string description)
    static bool DeleteRole(string name)
  */
  
  /**
   * Constructor.
   *
   * @return User
   */
  function User() {
    $this->UserId = 0;
    $this->UserName = '';
    $this->_PasswordHash = '';
    $this->FirstName = '';
    $this->LastName = '';
    $this->PhoneNumber = '';
    $this->Email = '';
    $this->Street1 = '';
    $this->Street2 = '';
    $this->Street3 = '';
    $this->City = '';
    $this->State = '';
    $this->Country = '';
    $this->PostalCode = '';
    $this->_Roles = array();
    $this->NewsletterSubscription = false;
    $this->Notes = '';
  }
  
  /**
   * Gets a subset of users, sorted by user id,
   * with an optional offset and maximum amount.
   *
   * @param integer
   * @param integer
   * @return Users[]
   */
  function GetUsers($offset = 0, $maxResults = 0) {
    $objects = array();
    $db = new DB();
    // Initial query
    $query = "SELECT u.UserId FROM users u ORDER BY u.UserId";
    // Look for max results and offsets
    if ($maxResults && $maxResults * 1 == $maxResults) {
      $query .= " LIMIT $maxResults";
      if ($offset && $offset * 1 == $offset)
        $query .= " OFFSET $offset";
    }
    elseif ($offset && $offset * 1 == $offset)
      $query .= " LIMIT 99999999 OFFSET $offset";
    // Get the ids
    $ids = $db->QueryCol($query);
    // If we have ids, load the objects
    if (is_array($ids))
      foreach ($ids as $id) {
        $objects[count($objects)] = new User();
        $objects[count($objects) - 1]->Load($id);
      }
    return $objects;
  }
  
  /**
   * Determines if a user exists or not.
   * 
   * @param integer
   * @return boolean
   */
  function Exists($userId) {
    $db = new DB();
    return ($db->QueryVal("SELECT u.UserId FROM users u WHERE u.UserId='" . addslashes($userId) . "' LIMIT 1;") > 0);
  }

  /**
   * Returns a string representation of the object.
   * 
   * @param string
   * @return string
   */
  function ToString($delimiter = "<br />") {
    $s = "";
    $s .= "UserId: " . $this->UserId . $delimiter;
    $s .= "UserName: " . $this->UserName . $delimiter;
    $s .= "PasswordHash: " . $this->_PasswordHash . $delimiter;
    $s .= "FirstName: " . $this->FirstName . $delimiter;
    $s .= "LastName: " . $this->LastName . $delimiter;
    $s .= "PhoneNumber: " . $this->PhoneNumber . $delimiter;
    $s .= "Street1: " . $this->Street1 . $delimiter;
    $s .= "Street2: " . $this->Street2 . $delimiter;
    $s .= "Street3: " . $this->Street3 . $delimiter;
    $s .= "City: " . $this->City . $delimiter;
    $s .= "State: " . $this->State . $delimiter;
    $s .= "Country: " . $this->Country . $delimiter;
    $s .= "PostalCode: " . $this->PostalCode . $delimiter;
    $s .= "Roles: [";
    if (is_array($this->_Roles))
      for ($i = 0; $i < count($this->_Roles); $i++)
        $s .= ($i ? ', ' : '') . $this->_Roles[$i];
    $s .= "]" . $delimiter;
    $s .= "NewsletterSubscription: " . ($this->NewsletterSubscription ? 'true' : 'false') . $delimiter;
    $s .= "Notes: " . $this->Notes . $delimiter;
    return $s;
  }

  /**
   * Loads a User; returns true on success, false on failure.
   *
   * @param integer
   * @return boolean
   */
  function Load($userId) {
    $db = new DB();
    // Prevent SQL injection once to ease coding later
    $userId = addslashes($userId);
    // Select bulk of the data
    $query = <<<HEREDOC
      SELECT
        u.UserId,
        u.UserName,
        u.PasswordHash,
        u.Email,
        ci.FirstName,
        ci.LastName,
        ci.PhoneNumber,
        ci.Street1,
        ci.Street2,
        ci.Street3,
        ci.City,
        ci.StateProvince AS State,
        ci.Country,
        ci.PostalCode,
        u.Newsletter,
        u.Notes
      FROM
        users u
      INNER JOIN
        contactinfo ci
      ON
        ci.ContactInfoId=u.ContactInfoId
      WHERE
        u.UserId={$userId}
HEREDOC;
    $data = $db->QueryRow($query);
    // If we can't find it, we can't load it
    if (!$data)
      return false;
    // Assign response
    $this->UserId = $data['UserId'];
    $this->UserName = $data['UserName'];
    $this->_PasswordHash = $data['PasswordHash'];
    $this->FirstName = $data['FirstName'];
    $this->LastName = $data['LastName'];
    $this->PhoneNumber = $data['PhoneNumber'];
    $this->Email = $data['Email'];
    $this->Street1 = $data['Street1'];
    $this->Street2 = $data['Street2'];
    $this->Street3 = $data['Street3'];
    $this->City = $data['City'];
    $this->State = $data['State'];
    $this->Country = $data['Country'];
    $this->PostalCode = $data['PostalCode'];
    $this->NewsletterSubscription = $data['Newsletter'];
    $this->Notes = $data['Notes'];

    // Now get the roles
    $query = "SELECT r.RoleName FROM roles r INNER JOIN usersroles ur ON r.RoleId=ur.RoleId WHERE ur.UserId='$userId';";
    $this->_Roles = $db->QueryCol($query);
    // Can't find roles? That's OK, it means there aren't any;
    // just make sure it's an empty array before leaving
    if (!$this->_Roles)
      $this->_Roles = array();

    // If we made it this far, we've loaded the object
    return true;
  }

  /**
   * Creates a user if unique, and returns the user id;
   * returns true on success, false on failure.
   * 
   * @param string
   * @param string
   * @param string
   * @return boolean
   */
  function Create($userName, $email, $password) {
    $userName = trim($userName);
    $email = trim($email);
    // Make sure we've got a user name and email
    if (!$userName || !$email)
      return false;
    // Make sure user doesn't exist
    elseif (User::Exists(User::GetUserIdByUserName($userName)))
      return false;

    $db = new DB();
    // Add a contactinfo record; it'll be empty now, but will be populated
    // when Save() gets called at the end of this method; we'll also need
    // to keep the contactinfo record id for when we add a new user record
    $sql = "INSERT INTO contactinfo VALUES ();";
    $db->Query($sql);
    $sql = "SELECT LAST_INSERT_ID();";
    $contactInfoId = $db->QueryVal($sql);

    // Create a new user record
    $this->UserName = $userName;
    $this->Email = $email;
    $this->SetPassword($password);
    $sql = "INSERT INTO users (UserName, ContactInfoId) VALUES ('" . addslashes($this->UserName) . "', '$contactInfoId');";
    $db->Query($sql);
    // Assign the newly-created id
    $sql = "SELECT LAST_INSERT_ID();";
    $this->UserId = $db->QueryVal($sql);

    // Verify the rest of the object data
    // (e.g. are the username & email both unique from other users?);
    // if we can't, remove the data and fail out of the Create
    if (!$this->Save()) {
      User::Delete($this->UserId);
      $this->UserId = 0;
      $this->UserName = '';
      $this->Email = '';
      $this->_PasswordHash = '';
      return false;
    }
    else
      return true;
  }
  
  /**
   * Saves a PRE-EXISTING User;
   * returns true on success, false on failure.
   *
   * @return boolean
   */
  function Save() {
    // "Fix" the data
    $this->UserName = trim($this->UserName);
    $this->FirstName = trim($this->FirstName);
    $this->LastName = trim($this->LastName);
    $this->PhoneNumber = trim($this->PhoneNumber);
    $this->Email = trim($this->Email);
    $this->Street1 = trim($this->Street1);
    $this->Street2 = trim($this->Street2);
    $this->Street3 = trim($this->Street3);
    $this->City = trim($this->City);
    $this->State = trim($this->State);
    $this->Country = trim($this->Country);
    $this->PostalCode = trim($this->PostalCode);
    $this->Notes = trim($this->Notes);

    // Ensure there's a username & email
    if (!$this->UserName || !$this->Email)
      return false;
    // Make sure the user exists
    if (!User::Exists($this->UserId))
      return false;
    // Make sure that any changes to the username/email are still unique;
    // the first WHERE clause makes sure we don't accidentally catch the same record
    $sql =
      "SELECT u.UserId " .
      "FROM users u " .
      "WHERE u.UserId<>'" . addslashes($this->UserId) . "' " .
      "AND (u.Email='" . addslashes($this->Email) . "' OR u.UserName='" . addslashes($this->UserName) . "') LIMIT 1;";
    // If we get a result, that means
    // another user has the email or username already,
    // and we shouldn't save to maintain uniqueness
    $db = new DB();
    if ($db->QueryVal($sql))
      return false;

    // Save the core user data
    $sql =
      "UPDATE users SET " .
        "UserName='" . addslashes($this->UserName) . "', " .
        "PasswordHash='" . addslashes($this->_PasswordHash) . "', " .
        "Email='" . addslashes($this->Email) . "', " .
        "Newsletter='" . ($this->NewsletterSubscription ? '1' : '0') . "', " .
        "Notes='" . addslashes($this->Notes) . "' " .
      "WHERE " .
        "UserId='" . addslashes($this->UserId) . "';";
    $db->Query($sql);
    // Save the contact info
    $contactInfoId = $db->QueryVal("SELECT u.ContactInfoId FROM users u WHERE u.UserId='" . addslashes($this->UserId) . "';");
    $sql =
      "UPDATE contactinfo SET " .
        "FirstName='" . addslashes($this->FirstName) . "', " .
        "LastName='" . addslashes($this->LastName) . "', " .
        "PhoneNumber='" . addslashes($this->PhoneNumber) . "', " .
        "Street1='" . addslashes($this->Street1) . "', " .
        "Street2='" . addslashes($this->Street2) . "', " .
        "Street3='" . addslashes($this->Street3) . "', " .
        "City='" . addslashes($this->City) . "', " .
        "StateProvince='" . addslashes($this->State) . "', " .
        "Country='" . addslashes($this->Country) . "', " .
        "PostalCode='" . addslashes($this->PostalCode) . "' " .
      "WHERE " .
        "ContactInfoId='" . addslashes($contactInfoId) . "';";
    $db->Query($sql);
    // Save the roles
    $sql = "DELETE FROM usersroles WHERE UserId='" . addslashes($this->UserId) . "'";
    $db->Query($sql);
    foreach ($this->_Roles as $role) {
      $sql = "SELECT r.RoleId FROM roles r WHERE r.RoleName='" . addslashes($role) . "';";
      $roleId = $db->QueryVal($sql);
      if ($roleId) {
        $sql = "INSERT INTO usersroles (UserId, RoleId) VALUES ('" . addslashes($this->UserId) . "', '" . addslashes($roleId) . "');";
        $db->Query($sql);
      }
    }
    // Yay! We did it!
    return true;
  }
  
  /**
   * Deletes a user; returns true on success, false on failure.
   *
   * @static
   * @param integer
   * @return boolean
   */
  function Delete($userId) {
    if (!User::Exists($userId))
      return false;
    $db = new DB();
    $contactInfoId = $db->QueryVal("SELECT u.ContactInfoId FROM users u WHERE u.UserId='" . addslashes($userId) . "'");
    $db->Query("DELETE FROM contactinfo WHERE ContactInfoId='" . addslashes($contactInfoId) . "';");
    $db->Query("DELETE FROM usersroles WHERE UserId='" . addslashes($userId) . "';");
    $db->Query("DELETE FROM users WHERE UserId='" . addslashes($userId) . "';");
    return true;
  }

  /**
   * Determines whether a user has submitted any donations.
   *
   * @return boolean
   */
  function HasDonationHistory() {
    $db = new DB();
    if ($db->QueryVal("SELECT d.DonationId FROM donations d WHERE d.UserId='" . addslashes($this->UserId) . "' LIMIT 1;"))
      return true;
    else
      return false;
  }
  
  /**
   * Gets a user's id based on the user name.
   *
   * @static
   * @param string
   * @return integer
   */
  function GetUserIdByUserName($userName) {
    $db = new DB();
    return $db->QueryVal("SELECT u.UserId FROM users u WHERE u.UserName='" . addslashes($userName) . "';");
  }
  
  /**
   * Gets a user's id based on the email.
   *
   * @static
   * @param string
   * @return integer
   */
  function GetUserIdByEmail($email) {
    $db = new DB();
    return $db->QueryVal("SELECT u.UserId FROM users u WHERE u.Email='" . addslashes($email) . "';");
  }
  
  /**
   * Sets a user's password;
   * does NOT modify the DB, must call Save to do that.
   *
   * @param string
   */
  function SetPassword($password) {
    $this->_PasswordHash = sha1($password);
  }
  
  /**
   * Returns true if password matches user's password, false otherwise.
   *
   * @param string
   * @return boolean
   */
  function ValidatePassword($password) {
    return ($this->_PasswordHash == sha1($password));
  }

  /**
   * Returns true if the user has the specified role;
   * the role can be a single role or an array of roles -
   * if it is an array, only 1 role needs to match.
   *
   * @param string | string[]
   * @return boolean
   */
  function HasRole($role) {
    $found = false;
    // Check array of roles
    if (is_array($role))
      for ($i = 0; $i < count($role) && !$found; $i++)
        $found = $this->HasRole($role[$i]);
    // Check single role
    else {
      $role = trim(strtolower($role));
      for ($i = 0; $i < count($this->_Roles) && !$found; $i++)
        $found = (trim(strtolower($this->_Roles[$i])) == $role);
    }
    return $found;
  }
  
  /**
   * Grants a user a specified role in the current object;
   * does NOT modify the DB, must call Save to do that;
   * returns true on success, false on failure.
   *
   * @param string
   * @return boolean
   */
  function GrantRole($role) {
    // Don't do jack if the user has the role already
    if ($this->HasRole($role))
      return false;
    $db = new DB();

    // This does more than get the proper case -
    // this also verifies that the role actually exists
    $role = $db->QueryVal("SELECT r.RoleName FROM roles r WHERE r.RoleName='" . addslashes($role) . "' LIMIT 1;");
    if (!$role)
      return false;

    // Remember, this doesn't modify the DB, $this->Save needs to be called
    $this->_Roles[count($this->_Roles)] = $role;
    return true;
  }
  
  /**
   * Revokes a specified role from a user; returns true on success, false on failure.
   *
   * @param string
   * @return boolean
   */
  function RevokeRole($role) {
    // Don't do jack if the user doesn't even have the role
    if (!$this->HasRole($role))
      return false;
    $db = new DB();

    // This does more than get the proper case -
    // this also verifies that the role actually exists
    $role = $db->QueryVal("SELECT r.RoleName FROM roles r WHERE r.RoleName='" . addslashes($role) . "' LIMIT 1;");
    if (!$role)
      return false;

    // Remember, this doesn't modify the DB, $this->Save needs to be called
    $newRoles = array();
    foreach ($this->_Roles as $newRole)
      if ($newRole != $role)
        $newRoles[count($newRoles)] = $newRole;
    $this->_Roles = $newRoles;
    return true;
  }
  
  /**
   * Revokes all roles.
   */
  function RevokeAllRoles() {
    foreach ($this->_Roles as $role)
      $this->RevokeRole($role);
  }
  
  /**
   * Gets all roles in the DB;
   * not specific to users, but helpful nonetheless.
   *
   * @static
   * @return string[]
   */
  function GetRoles() {
    $db = new DB();
    $roles = $db->QueryCol("SELECT r.RoleName FROM roles r ORDER BY r.RoleId;");
    if (!$roles)
      $roles = array();
    return $roles;
  }
  
  /**
   * Creates a unique role in the DB;
   * not specific to users, but helpful nonetheless;
   * returns true on success, false on failure.
   *
   * @static
   * @param string
   * @param string
   * @return string[]
   */
  function CreateRole($name, $description) {
    $name = addslashes(trim($name));
    $description = addslashes(trim($description));
    $db = new DB();
    $roleId = $db->QueryVal("SELECT r.RoleId FROM roles r WHERE r.RoleName='$name';");
    if ($roleId)
      return false;
    $db->Query("INSERT INTO roles (RoleName, RoleDescription) VALUES ('$name', '$description');");
    return true;
  }

  /**
   * Removes an existing role in the DB and any ties to users it may have;
   * not specific to users, but helpful nonetheless;
   * returns true on success, false on failure.
   *
   * @static
   * @param string
   * @return string[]
   */
  function DeleteRole($name) {
    $name = addslashes(trim($name));
    $db = new DB();
    $roleId = $db->QueryVal("SELECT r.RoleId FROM roles r WHERE r.RoleName='$name';");
    if (!$roleId)
      return false;
    $db->Query("DELETE FROM usersroles WHERE RoleId='$roleId';");
    $db->Query("DELETE FROM roles WHERE RoleId='$roleId';");
    return true;
  }
}